package com.talis.platform.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.log4j.Logger;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.talis.http.HttpHeaders;
import com.talis.http.MediaTypes;
import com.talis.http.Response;

public class Metabox {

    private static final Logger log = Logger.getLogger(Metabox.class);

    private String myUri;
    private Credentials myCredentials;

    public Metabox(String uri) {
        myUri = uri;
    }

    public Metabox(String uri, Credentials credentials) {
        myUri = uri;
        myCredentials = credentials;
    }

    public String getUri() {
        return myUri;
    }

    public Credentials getCredentials() {
        return myCredentials;
    }

    public Response applyChangesetRdfXml(InputStream changeset) throws HttpException, IOException, ClientException, ServiceException{
        return applyChangesetRdfXml(myUri, changeset);
    }
    
    public Response applyVersionedChangesetRdfXml(InputStream changeset) throws HttpException, IOException, ClientException, ServiceException{
        return applyChangesetRdfXml(myUri + "/changesets", changeset);
    }
    
    private Response applyChangesetRdfXml(String uri, InputStream changeset) throws HttpException, IOException, ClientException, ServiceException{
        if (log.isDebugEnabled()) { log.debug(String.format("POSTing changeset rdf/xml to %s", uri)); }
        HttpClient client = getClient();
        PostMethod post = new PostMethod(uri);
        post.setRequestEntity(new InputStreamRequestEntity(changeset));
        post.addRequestHeader(HttpHeaders.CONTENT_TYPE, MediaTypes.CHANGESET);
        post.addRequestHeader(HttpHeaders.ACCEPT, "*/*");
        client.executeMethod(post);
        if (log.isDebugEnabled()) { log.debug(String.format("POSTed changeset rdf/xml to %s, building response", uri)); }
        
        return  buildResponseOrThrow(post);
    }
    
    public Response applyChangeset( final Model model) throws HttpException, IOException, ClientException, ServiceException{
        return applyChangeset(myUri, model);
    }
    
    public Response applyVersionedChangeset( final Model model) throws HttpException, IOException, ClientException, ServiceException {
        return applyChangeset(myUri + "/changesets", model);
    }
    
    private Response applyChangeset(String uri, final Model model) throws HttpException, IOException, ClientException, ServiceException{
        if (log.isDebugEnabled()) { log.debug(String.format("POSTing changeset rdf/xml to %s", uri)); }
        HttpClient client = getClient();
        
        InputStream in = new DelayedInputStream ( 
                new DelayedStreamable(){
                    public void write(OutputStream out) {
                        model.write(out);
                    }
                });
        
        PostMethod post = new PostMethod(uri);
        post.setRequestEntity(new InputStreamRequestEntity(in));
        post.addRequestHeader(HttpHeaders.CONTENT_TYPE, MediaTypes.CHANGESET);
        post.addRequestHeader(HttpHeaders.ACCEPT, "*/*");
        client.executeMethod(post);
        if (log.isDebugEnabled()) { log.debug(String.format("POSTed changeset rdf/xml to %s, building response", uri)); }
        
        return  buildResponseOrThrow(post);
    }

    public Response submitRdfXml(InputStream rdfXml) throws HttpException, IOException, ClientException, ServiceException{
        if (log.isDebugEnabled()) { log.debug(String.format("POSTing rdf/xml to %s", myUri)); }
        HttpClient client = getClient();
        PostMethod post = new PostMethod(myUri);
        post.setRequestEntity(new InputStreamRequestEntity(rdfXml));
        post.addRequestHeader(HttpHeaders.CONTENT_TYPE, MediaTypes.RDFXML);
        post.addRequestHeader(HttpHeaders.ACCEPT, "*/*");
        client.executeMethod(post);
        if (log.isDebugEnabled()) { log.debug(String.format("POSTed rdf/xml to %s", myUri)); }
        
        return  buildResponseOrThrow(post);
    }
    
    public Response submitModel(final Model model) throws HttpException, IOException, ClientException, ServiceException{
        if (log.isDebugEnabled()) { log.debug(String.format("POSTing rdf/xml to %s", myUri)); }
        HttpClient client = getClient();
        
        InputStream in = new DelayedInputStream ( 
                new DelayedStreamable(){
                    public void write(OutputStream out) {
                        model.write(out);
                    }
                });
        
        PostMethod post = new PostMethod(myUri);
        post.setRequestEntity(new InputStreamRequestEntity(in));
        post.addRequestHeader(HttpHeaders.CONTENT_TYPE, MediaTypes.RDFXML);
        post.addRequestHeader(HttpHeaders.ACCEPT, "*/*");
        client.executeMethod(post);
        if (log.isDebugEnabled()) { log.debug(String.format("POSTed rdf/xml to %s", myUri)); }
        
        return buildResponseOrThrow(post);
    }

    public Response describe(String subject) throws HttpException, IOException, ClientException, ServiceException{
        if (log.isDebugEnabled()) { log.debug(String.format("GETting description of resource %s from %s", subject, myUri)); }
        Response response = describe(subject, null);
        if (log.isDebugEnabled()) { log.debug("Done GETting resource description"); }
        return response;
    }
    
    public Response describe(String subject, String mediaType) throws HttpException, IOException, ClientException, ServiceException{
        if (log.isDebugEnabled()) { log.debug(String.format("GETting description of resource %s from %s", subject, myUri)); }
        try {
            subject = URLEncoder.encode(subject, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            log.warn("Could not urlencode subject uri", e);
        }
        
        String uri = myUri + "?about=" + subject;
        
        HttpClient client = getClient();
        GetMethod get = new GetMethod(uri);
        get.addRequestHeader(HttpHeaders.CONTENT_TYPE, MediaTypes.FORM_ENCODED);
        
        if (mediaType == null) {
        	get.addRequestHeader(HttpHeaders.ACCEPT, MediaTypes.RDFXML);
        } else {
        	get.addRequestHeader(HttpHeaders.ACCEPT, mediaType);
        }
        
        client.executeMethod(get);
        if (log.isDebugEnabled()) { log.debug("Done GETting resource description"); }
        
        return buildResponseOrThrow(get);
    }
    
    public InputStream describeAsStream(String subject, String mediaType) throws HttpException, IOException, ClientException, ServiceException {
    	Response response = describe(subject, mediaType);
    	return response.getEntityBody();
    }
    
    public Model describeAsModel(String subject) throws HttpException, IOException, ClientException, ServiceException {
    	Response response = describe(subject);
    	Model model = ModelFactory.createDefaultModel();
    	model.read( response.getEntityBody(), "");    	
    	return model;
    }
    
    public boolean hasDescription(String subject) throws HttpException, IOException, 
                                                    UndeterminedResultException, ServiceException, 
                                                    ClientException{
        
        if (log.isDebugEnabled()) { log.debug(String.format("Checking for description of resource %s from %s", subject, myUri)); }
        Response response = describe(subject);
        if (response.getStatus() == 200){
            Model description = ModelFactory.createDefaultModel();
            description.read( response.getEntityBody(), "");    
            return ! description.isEmpty();
        }
        else if (response.getStatus() >= 499 && response.getStatus() <= 499){
            throw new ClientException("The service returned a client side error response", response);
        }else if (response.getStatus() >= 500 && response.getStatus() <= 599){
            throw new ServiceException("The service returned a service side error response", response);
        }else{
            throw new UndeterminedResultException(String.format("Unable to determine answer from HTTP Response.", 
                                                  response.getStatus(), 
                                                  new String(response.getEntityBodyAsByteArray()))); 
        }
    }
    
    private Response buildResponse(HttpMethod method) throws IOException{
        Response response = new Response(method.getStatusCode());
        for (Header header: method.getResponseHeaders()){
            response.addHeader(header.getName(), header.getValue());
        }
        response.setEntityBody(method.getResponseBodyAsStream());
        if (log.isDebugEnabled()) { log.debug("Built response"); }
        return response;
    }
    
    private Response buildResponseOrThrow(HttpMethod method) throws IOException, ClientException, ServiceException{
        Response response = buildResponse(method);
        if (method.getStatusCode() >= 400 && method.getStatusCode() <= 499){
            throw new ClientException("The service returned a client side error response", response);
        }else if (method.getStatusCode() >= 500 && method.getStatusCode() <= 599){
            throw new ServiceException("The service returned a service side error response", response);
        }
        return response;
    }
    
    private HttpClient getClient() {
    	HttpClient client = new HttpClient();
        client.getState().setCredentials( AuthScope.ANY, myCredentials );
        return client;
    }
}
